package cn.iocoder.yudao.module.system.service.sms;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO;
import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsCodeMapper;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.system.framework.sms.config.SmsCodeProperties;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.time.LocalDateTime;

import static cn.hutool.core.util.RandomUtil.randomInt;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.isToday;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;

/**
 * 短信验证码 Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Validated
public class SmsCodeServiceImpl implements SmsCodeService {

    @Resource
    private SmsCodeProperties smsCodeProperties;

    @Resource
    private SmsCodeMapper smsCodeMapper;

    @Resource
    private SmsSendService smsSendService;

    @Override
    public void sendSmsCode(SmsCodeSendReqDTO reqDTO) {
        SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene());
        Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene());
        // 创建验证码
        String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp());
        // 发送验证码
        smsSendService.sendSingleSms(reqDTO.getMobile(), null, null,
                sceneEnum.getTemplateCode(), MapUtil.of("code", code));
    }

    // 定义一个私有方法 createSmsCode，它接收手机号、场景和IP地址作为参数，并返回一个字符串（验证码）。
    private String createSmsCode(String mobile, Integer scene, String ip) {

        // 查询最后一次发送给该手机号的验证码记录，不限制场景和时间。
        SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null);

        // 如果存在最后一次发送的验证码记录。
        if (lastSmsCode != null) {

            // 计算最后发送验证码的时间与当前时间的间隔（毫秒）。
            // 如果间隔小于配置中允许的最小发送频率，抛出“发送过于频繁”的异常。
            if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
                    < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
                throw exception(SMS_CODE_SEND_TOO_FAST);
            }

            // 如果最后发送验证码的日期是今天。
            if (isToday(lastSmsCode.getCreateTime()) &&
                    // 并且今天发送的验证码数量已经达到或超过最大允许数量。
                    lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。
                // 抛出“超过当天发送上限”的异常。
                throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
            }

            // TODO 芋艿：提升，每个 IP 每天可发送数量
            // TODO 芋艿：提升，每个 IP 每小时可发送数量
        }

        // 生成一个指定长度的随机验证码，长度与配置中的结束码长度相同。
        String code = String.format("%0" + smsCodeProperties.getEndCode().toString().length() + "d",
                randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1));

        // 构建一个新的验证码记录对象，并设置其属性。
        SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene)
                .todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1)
                .createIp(ip).used(false).build();

        // 将新的验证码记录插入到数据库中。
        smsCodeMapper.insert(newSmsCode);

        // 返回生成的验证码。
        return code;
    }


    @Override
    public void useSmsCode(SmsCodeUseReqDTO reqDTO) {
        // 检测验证码是否有效
        SmsCodeDO lastSmsCode = validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
        // 使用验证码
        smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId())
                .used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build());
    }

    @Override
    public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) {
        validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
    }

    // 定义一个私有方法 validateSmsCode0，它接收手机号、验证码和场景作为参数，并返回一个 SmsCodeDO 对象。
    private SmsCodeDO validateSmsCode0(String mobile, String code, Integer scene) {

        // 查询与手机号、验证码和场景匹配的最后一条验证码记录。
        SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene);

        // 如果没有找到匹配的验证码记录，抛出“验证码不存在”的异常。
        if (lastSmsCode == null) {
            throw exception(SMS_CODE_NOT_FOUND);
        }

        // 计算最后发送验证码的时间与当前时间的间隔（毫秒）。
        // 如果间隔大于或等于配置中指定的验证码过期时间，抛出“验证码已过期”的异常。
        if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
                >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期
            throw exception(SMS_CODE_EXPIRED);
        }

        // 检查验证码是否已被使用（即它的 used 属性是否为 true）。
        // 如果验证码已被使用，抛出“验证码已被使用”的异常。
        if (Boolean.TRUE.equals(lastSmsCode.getUsed())) {
            throw exception(SMS_CODE_USED);
        }

        // 如果以上校验都通过，返回找到的验证码记录对象。
        return lastSmsCode;
    }

}
